/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.geode.cache30;

import org.junit.experimental.categories.Category;
import org.junit.Test;

import static org.junit.Assert.*;

import org.apache.geode.test.dunit.cache.internal.JUnit4CacheTestCase;
import org.apache.geode.test.dunit.internal.JUnit4DistributedTestCase;
import org.apache.geode.test.junit.categories.DistributedTest;

import org.apache.geode.cache.*;
//import org.apache.geode.cache.util.*;
//import java.util.*;

/**
 * An abstract class whose test methods test the functionality of
 * {@link CacheWriter}s that are invoked locally.
 *
 * @see MultiVMRegionTestCase#testRemoteCacheWriter
 *
 *
 * @since GemFire 3.0
 */
public abstract class CacheWriterTestCase
  extends RegionAttributesTestCase {

  public CacheWriterTestCase() {
    super();
  }

  ///////////////////////  Test Methods  ///////////////////////

  /**
   * Tests that the <code>CacheWriter</code> is called before an entry
   * is {@linkplain CacheWriter#beforeCreate created}.
   */
  @Test
  public void testCacheWriterBeforeCreate() throws CacheException {
    String name = this.getUniqueName();
    final Object key = this.getUniqueName();
    final Object value = new Integer(42);
    final Object arg = "ARG";
    final String exception = "EXCEPTION";

    TestCacheWriter writer = new TestCacheWriter() {
        public void beforeCreate2(EntryEvent event)
          throws CacheWriterException {

          assertEquals(key, event.getKey());
          assertEquals(value, event.getNewValue());
          assertNull(event.getOldValue());
          assertTrue(event.getOperation().isCreate());
          assertFalse(event.getOperation().isLoad());
          assertFalse(event.getOperation().isLocalLoad());
          assertFalse(event.getOperation().isNetLoad());
          assertFalse(event.getOperation().isNetSearch());

          Object argument = event.getCallbackArgument();
          if (argument != null) {
            if (argument.equals(exception)) {
              String s = "Test CacheWriterException";
              throw new CacheWriterException(s);

            } else {
              assertEquals(arg, argument);
            }
          }
        }

        public void beforeDestroy2(EntryEvent event)
          throws CacheWriterException {
          // This method will get invoked when the region is populated
        }
      };

    AttributesFactory factory =
      new AttributesFactory(getRegionAttributes());
    factory.setCacheWriter(writer);
    Region region =
      createRegion(name, factory.create());

    region.create(key, value);
    assertTrue(writer.wasInvoked());
    region.destroy(key);
    assertTrue(writer.wasInvoked());

    region.put(key, value);
    assertTrue(writer.wasInvoked());
    region.destroy(key);
    assertTrue(writer.wasInvoked());

    region.create(key, value, arg);
    assertTrue(writer.wasInvoked());
    region.destroy(key);
    assertTrue(writer.wasInvoked());

    region.put(key, value, arg);
    assertTrue(writer.wasInvoked());
    region.destroy(key);
    assertTrue(writer.wasInvoked());

    try {
      region.create(key, value, exception);
      fail("Should have thrown a CacheWriterException");

    } catch (CacheWriterException ex) {
      // pass...
      assertTrue(writer.wasInvoked());
    }

    try {
      region.put(key, value, exception);
      fail("Should have thrown a CacheWriterException");

    } catch (CacheWriterException ex) {
      // pass...
      assertTrue(writer.wasInvoked());
    }

  }

  /**
   * Tests that the <code>CacheWriter</code> is called before an entry
   * is {@linkplain CacheWriter#beforeUpdate updated}.
   */
  @Test
  public void testCacheWriterBeforeUpdate() throws CacheException {
    String name = this.getUniqueName();
    final Object key = this.getUniqueName();
    final Object oldValue = new Integer(42);
    final Object newValue = new Integer(43);
    final Object arg = "ARG";
    final String exception = "EXCEPTION";

    TestCacheWriter writer = new TestCacheWriter() {
        public void beforeCreate2(EntryEvent event)
          throws CacheWriterException {
          // This method will get invoked when the region is populated
        }

        public void beforeDestroy2(EntryEvent event)
          throws CacheWriterException {
          // This method will get invoked when the region is populated
        }

        public void beforeUpdate2(EntryEvent event)
          throws CacheWriterException {

          assertEquals(key, event.getKey());
          assertEquals(newValue, event.getNewValue());
          assertEquals(oldValue, event.getOldValue());
          assertTrue(event.getOperation().isUpdate());
          assertFalse(event.getOperation().isLoad());
          assertFalse(event.getOperation().isLocalLoad());
          assertFalse(event.getOperation().isNetLoad());
          assertFalse(event.getOperation().isNetSearch());

          Object argument = event.getCallbackArgument();
          if (argument != null) {
            if (argument.equals(exception)) {
              String s = "Test CacheWriterException";
              throw new CacheWriterException(s);

            } else {
              assertEquals(arg, argument);
            }
          }
        }
      };

    AttributesFactory factory =
      new AttributesFactory(getRegionAttributes());
    factory.setCacheWriter(writer);
    Region region =
      createRegion(name, factory.create());

    region.create(key, oldValue);
    assertTrue(writer.wasInvoked());
    region.put(key, newValue);
    assertTrue(writer.wasInvoked());
    region.destroy(key);
    assertTrue(writer.wasInvoked());

    region.put(key, oldValue);
    assertTrue(writer.wasInvoked());
    region.put(key, newValue);
    assertTrue(writer.wasInvoked());
    region.destroy(key);
    assertTrue(writer.wasInvoked());

    region.create(key, oldValue);
    assertTrue(writer.wasInvoked());
    region.put(key, newValue, arg);
    assertTrue(writer.wasInvoked());
    region.destroy(key);
    assertTrue(writer.wasInvoked());

    region.put(key, oldValue);
    assertTrue(writer.wasInvoked());
    region.put(key, newValue, arg);
    assertTrue(writer.wasInvoked());
    region.destroy(key);
    assertTrue(writer.wasInvoked());

    region.create(key, oldValue);
    assertTrue(writer.wasInvoked());
    try {
      region.put(key, newValue, exception);
      fail("Should have thrown a CacheWriterException");

    } catch (CacheWriterException ex) {
      // pass...
      assertTrue(writer.wasInvoked());
    }
    region.destroy(key);
    assertTrue(writer.wasInvoked());

    region.create(key, oldValue);
    assertTrue(writer.wasInvoked());
    try {
      region.put(key, newValue, exception);
      fail("Should have thrown a CacheWriterException");

    } catch (CacheWriterException ex) {
      // pass...
      assertTrue(writer.wasInvoked());
    }

  }

  /**
   * Tests that the <code>CacheWriter</code> is called before an entry
   * is {@linkplain CacheWriter#beforeDestroy destroyed}.
   */
  @Test
  public void testCacheWriterBeforeDestroy() throws CacheException {
    String name = this.getUniqueName();
    final Object key = this.getUniqueName();
    final Object value = new Integer(42);
    final Object arg = "ARG";
    final String exception = "EXCEPTION";

    TestCacheWriter writer = new TestCacheWriter() {
        public void beforeCreate2(EntryEvent event)
          throws CacheWriterException {
          // This method will get invoked when the region is populated
        }

        public void beforeDestroy2(EntryEvent event)
          throws CacheWriterException {

          assertEquals(key, event.getKey());
          assertEquals(value, event.getOldValue());
          assertNull(event.getNewValue());
          assertTrue(event.getOperation().isDestroy());
          assertFalse(event.getOperation().isLoad());
          assertFalse(event.getOperation().isLocalLoad());
          assertFalse(event.getOperation().isNetLoad());
          assertFalse(event.getOperation().isNetSearch());

          Object argument = event.getCallbackArgument();
          if (argument != null) {
            if (argument.equals(exception)) {
              String s = "Test CacheWriterException";
              throw new CacheWriterException(s);

            } else {
              assertEquals(arg, argument);
            }
          }
        }
      };

    AttributesFactory factory =
      new AttributesFactory(getRegionAttributes());
    factory.setCacheWriter(writer);
    Region region =
      createRegion(name, factory.create());

    region.create(key, value);
    assertTrue(writer.wasInvoked());
    region.destroy(key);
    assertTrue(writer.wasInvoked());

    region.create(key, value);
    assertTrue(writer.wasInvoked());
    region.destroy(key, arg);
    assertTrue(writer.wasInvoked());

    region.create(key, value);
    assertTrue(writer.wasInvoked());
    try {
      region.destroy(key, exception);
      fail("Should have thrown a CacheWriterException");

    } catch (CacheWriterException ex) {
      // pass...
      assertTrue(writer.wasInvoked());
    }
  }

  /**
   * Tests that the <code>CacheWriter</code> is called before a region
   * is destroyed.
   * 
   * @see CacheWriter#beforeRegionDestroy
   * @see CacheWriter#close
   */
  @Test
  public void testCacheWriterBeforeRegionDestroy()
    throws CacheException {

    final String name = this.getUniqueName();
    final Object arg = "ARG";
    final String exception = "EXCEPTION";

    TestCacheWriter writer = new TestCacheWriter() {
        private boolean closed = false;
        private boolean destroyed = false;

        public boolean wasInvoked() {
          boolean value = closed && destroyed;
          super.wasInvoked();
          return value;
        }
        
        public void close2() {
          this.closed = true;
        }

        public void beforeRegionDestroy2(RegionEvent event)
          throws CacheWriterException {

          assertEquals(name, event.getRegion().getName());
          // this should be a distributed destroy unless the region
          // is local scope
          assertTrue(event.getOperation().isRegionDestroy());
          assertFalse(event.getOperation().isExpiration());
          assertFalse(event.isOriginRemote());

          Object argument = event.getCallbackArgument();
          if (argument != null) {
            if (argument.equals(exception)) {
              String s = "Test CacheWriterException";
              throw new CacheWriterException(s);

            } else {
              assertEquals(arg, argument);
            }
          }

          this.destroyed = true;
        }
      };

    AttributesFactory factory =
      new AttributesFactory(getRegionAttributes());
    factory.setCacheWriter(writer);
    RegionAttributes attrs = factory.create();
    Region region;

    region = createRegion(name, attrs);
    region.destroyRegion();
    assertTrue(region.isDestroyed());
    assertTrue(writer.wasInvoked());


    region = createRegion(name, attrs);
    region.destroyRegion(arg);
    assertTrue(writer.wasInvoked());
    assertTrue(region.isDestroyed());


    try {
      region = createRegion(name, attrs);
      region.destroyRegion(exception);
      fail("Should have thrown a CacheWriterException");

    } catch (CacheWriterException ex) {
      // pass...
      assertTrue(writer.wasInvoked());
      assertFalse(region.isDestroyed());
      assertNull(region.getSubregion(name));
    }
  }

  /**
   * Tests that a <code>CacheWriter</code> is <I>not</I> invoked on a
   * {@linkplain Region#localDestroyRegion local destroy}.
   */
  @Test
  public void testCacheWriterLocalDestroy() throws CacheException {
    final String name = this.getUniqueName();

    // If any of the writer's callback methods are invoked
    TestCacheWriter writer = new TestCacheWriter() { };

    AttributesFactory factory =
      new AttributesFactory(getRegionAttributes());
    factory.setCacheWriter(writer);
    RegionAttributes attrs = factory.create();
    Region region = createRegion(name, attrs);
    region.localDestroyRegion();
  }

  /**
   * Tests that a {@link CacheWriter} throwing a {@link
   * CacheWriterException} aborts the operation.
   */
  @Test
  public void testCacheWriterExceptionAborts() throws CacheException {
    final String name = this.getUniqueName();
    final String exception = "EXCEPTION";

    TestCacheWriter writer = new TestCacheWriter() {
        private void handleEvent(Object argument)
          throws CacheWriterException {

          if (exception.equals(argument)) {
            String s = "Test Exception";
            throw new CacheWriterException(s);
          }
        }

        public void beforeCreate2(EntryEvent event)
          throws CacheWriterException {

          handleEvent(event.getCallbackArgument());
        }

        public void beforeUpdate2(EntryEvent event)
          throws CacheWriterException {

          handleEvent(event.getCallbackArgument());
        }

        public void beforeDestroy2(EntryEvent event)
          throws CacheWriterException {

          handleEvent(event.getCallbackArgument());
        }

        public void beforeRegionDestroy2(RegionEvent event)
          throws CacheWriterException {

          handleEvent(event.getCallbackArgument());
        }
      };

    AttributesFactory factory =
      new AttributesFactory(getRegionAttributes());
    factory.setCacheWriter(writer);
    RegionAttributes attrs = factory.create();
    Region region;

    region = createRegion(name, attrs);
    Object value = new Integer(42);

    String p1 = "Test Exception";
    getCache().getLogger().info("<ExpectedException action=add>"
        + p1 + "</ExpectedException>");
    try {
      region.put(name, value, exception);
      fail("Should have thrown a CacheWriterException");

    } catch (CacheWriterException ex) {
      assertNull(region.getEntry(name));
    } finally {
      getCache().getLogger().info("<ExpectedException action=remove>"
          + p1 + "</ExpectedException>");
    }

    region.put(name, value);

    try {
      region.put(name, "NEVER SEEN", exception);
      fail("Should have thrown a CacheWriterException");

    } catch (CacheWriterException ex) {
      Region.Entry entry = region.getEntry(name);
      assertNotNull(entry);
      assertEquals(value, entry.getValue());
    }

    try {
      region.destroy(name, exception);
      fail("Should have thrown a CacheWriterException");

    } catch (CacheWriterException ex) {
      Region.Entry entry = region.getEntry(name);
      assertNotNull(entry);
      assertEquals(value, entry.getValue());
    }

    try {
      region.destroyRegion(exception);

    } catch (CacheWriterException ex) {
      assertTrue(!region.isDestroyed());
      assertNotNull(region.getParentRegion().getSubregion(name));
    }
  }

}
